/******************************************************************************
*  Copyright(c) 2021-2023 Sietium
*
*  @file:    depth.cpp
*  @brif:    depth test
*
*  @date:    05/10/2021
*  @Author:  cary.Zhang
*
******************************************************************************/
#include <string>
#include <cstdlib>
#include <vector>
#include <memory>
#include "vmath.h"
#include "glad/gl-headers.h"

#include "texture.h"
#include "node.h"
#include "objectfactory.h"


class Depth : public Node
{
public:
    Depth();
protected:
    bool startup();
    void render(double currentTime, double difTime);
    void shutdown();
private:
    void renderCube(double currentTime, double difTime);
    void renderCircle(double currentTime, double difTime);
private:
    GLSLProgram programCube;
    GLSLProgram programCircle;

    float verticesCube[108] = {
        /* Front face. */
        /* Bottom left */
        -0.5,  0.5, -0.5,
            0.5, -0.5, -0.5,
            -0.5, -0.5, -0.5,
            /* Top right */
            -0.5,  0.5, -0.5,
            0.5,  0.5, -0.5,
            0.5, -0.5, -0.5,
            /* Left face */
            /* Bottom left */
            -0.5,  0.5,  0.5,
            -0.5, -0.5, -0.5,
            -0.5, -0.5,  0.5,
            /* Top right */
            -0.5,  0.5,  0.5,
            -0.5,  0.5, -0.5,
            -0.5, -0.5, -0.5,
            /* Top face */
            /* Bottom left */
            -0.5,  0.5,  0.5,
            0.5,  0.5, -0.5,
            -0.5,  0.5, -0.5,
            /* Top right */
            -0.5,  0.5,  0.5,
            0.5,  0.5,  0.5,
            0.5,  0.5, -0.5,
            /* Right face */
            /* Bottom left */
            0.5,  0.5, -0.5,
            0.5, -0.5,  0.5,
            0.5, -0.5, -0.5,
            /* Top right */
            0.5,  0.5, -0.5,
            0.5,  0.5,  0.5,
            0.5, -0.5,  0.5,
            /* Back face */
            /* Bottom left */
            0.5,  0.5,  0.5,
            -0.5, -0.5,  0.5,
            0.5, -0.5,  0.5,
            /* Top right */
            0.5,  0.5,  0.5,
            -0.5,  0.5,  0.5,
            -0.5, -0.5,  0.5,
            /* Bottom face */
            /* Bottom left */
            -0.5, -0.5, -0.5,
            0.5, -0.5,  0.5,
            -0.5, -0.5,  0.5,
            /* Top right */
            -0.5, -0.5, -0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5,  0.5,
        };

    const float colorsCube[108] = {
        /* Front face */
        /* Bottom left */
        1.0, 0.0, 0.0, /* red */
        1.0, 0.0, 0.0, /* red */
        1.0, 0.0, 0.0, /* red */
        /* Top right */
        1.0, 0.0, 0.0, /* red */
        1.0, 0.0, 0.0, /* red */
        1.0, 0.0, 0.0, /* red */

        /* Left face */
        /* Bottom left */
        0.0, 0.0, 1.0, /* blue */
        0.0, 0.0, 1.0, /* blue */
        0.0, 0.0, 1.0, /* blue */
        /* Top right */
        0.0, 0.0, 1.0, /* blue */
        0.0, 0.0, 1.0, /* blue */
        0.0, 0.0, 1.0, /* blue */

        /* Top face */
        /* Bottom left */
        1.0, 1.0, 1.0, /* white */
        1.0, 1.0, 1.0, /* white */
        1.0, 1.0, 1.0, /* white */
        /* Top right */
        1.0, 1.0, 1.0, /* white */
        1.0, 1.0, 1.0, /* white */
        1.0, 1.0, 1.0, /* white */

        /* Right face */
        /* Bottom left */
        1.0, 1.0, 0.0, /* yellow */
        1.0, 1.0, 0.0, /* yellow */
        1.0, 1.0, 0.0, /* yellow */
        /* Top right */
        1.0, 1.0, 0.0, /* yellow */
        1.0, 1.0, 0.0, /* yellow */
        1.0, 1.0, 0.0, /* yellow */

        /* Back face */
        /* Bottom left */
        0.0, 1.0, 1.0, /* cyan */
        0.0, 1.0, 1.0, /* cyan */
        0.0, 1.0, 1.0, /* cyan */
        /* Top right */
        0.0, 1.0, 1.0, /* cyan */
        0.0, 1.0, 1.0, /* cyan */
        0.0, 1.0, 1.0, /* cyan */

        /* Bottom face */
        /* Bottom left */
        1.0, 0.0, 1.0, /* magenta */
        1.0, 0.0, 1.0, /* magenta */
        1.0, 0.0, 1.0, /* magenta */
        /* Top right */
        1.0, 0.0, 1.0, /* magenta */
        1.0, 0.0, 1.0, /* magenta */
        1.0, 0.0, 1.0, /* magenta */
    };

    std::vector<GLfloat> verticesMirror;

    GLuint fbo;
    GLuint vao1;
    GLuint buffer1;
    GLuint vao2;
    GLuint buffer2;
    GLuint texture;
};

Depth::Depth()
{
    nodeName = "Depth";
}

bool Depth::startup()
{
    if (PerfWindow::glVersion < 330) {
        printf("Failed! The GL version is less than 3.3 !\n");
        return false;
              }

    bool ok = programCube.build("../media/shaders/depth/depth_cube.vert",
                                "../media/shaders/depth/depth_cube.frag");
    if (!ok)
        return false;

    ok = programCircle.build("../media/shaders/depth/depth_glass.vert",
                             "../media/shaders/depth/depth_glass.frag");
    if (!ok)
        return false;

    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    glGenTextures(1, &texture);

    /* Texture unit 1 for depth buffer */
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, getWidth(), getHeight(), 0, GL_DEPTH_COMPONENT,
                 GL_UNSIGNED_INT, NULL);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture, 0);

    if (GL_FRAMEBUFFER_COMPLETE != glCheckFramebufferStatus(GL_FRAMEBUFFER))
        printf("glCheckFramebufferStatue failed\n");

    glGenVertexArrays(1, &vao1);
    glBindVertexArray(vao1);

    glGenBuffers(1, &buffer1);
    glBindBuffer(GL_ARRAY_BUFFER, buffer1);
    glBufferData(GL_ARRAY_BUFFER, sizeof(verticesCube) + sizeof(colorsCube), NULL, GL_STATIC_DRAW);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verticesCube), verticesCube);
    glBufferSubData(GL_ARRAY_BUFFER, sizeof(verticesCube), sizeof(colorsCube), colorsCube);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(sizeof(verticesCube)));

    const int count = 1200;
    const float degree = 2 * 3.14156265 / (float)count;

    verticesMirror.resize(count * 3);
    verticesMirror[0] = 0.f;
    verticesMirror[1] = 0.f;
    verticesMirror[2] = 1.0f;
    for (int i = 0; i < count; i++) {
        verticesMirror[i * 3 + 0] = 0.5 * cos(degree * i);
        verticesMirror[i * 3 + 1] = 0.5 * sin(degree * i);
        verticesMirror[i * 3 + 2] = 1.0f;
    }

    glGenVertexArrays(1, &vao2);
    glBindVertexArray(vao2);

    glGenBuffers(1, &buffer2);
    glBindBuffer(GL_ARRAY_BUFFER, buffer2);
    gl_BufferData(GL_ARRAY_BUFFER, verticesMirror, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    return true;
}

void Depth::shutdown()
{
    glDeleteVertexArrays(1, &vao1);
    glDeleteVertexArrays(1, &vao2);
    glDeleteFramebuffers(1, &fbo);
    glDeleteBuffers(1, &buffer1);
    glDeleteBuffers(1, &buffer2);
    glDeleteTextures(1, &texture);

    programCube.clear();
    programCircle.clear();

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glUseProgram(0);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindVertexArray(0);

    glBlendFunc(GL_ONE, GL_ZERO);
    glDisable(GL_BLEND);
    glDisable(GL_CULL_FACE);
    glDisable(GL_DEPTH_TEST);
}

void Depth::render(double currentTime, double difTime)
{
    (void)difTime;

    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    /* Should do: src * (src alpha) + dest * (1-src alpha). */
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glClearColor(0.6f, 0.7f, 0.8f, 1.0f);

    renderCube(currentTime, difTime);
    renderCircle(currentTime, difTime);

    glDisable(GL_CULL_FACE);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_BLEND);
}

void Depth::renderCube(double currentTime, double difTime)
{
    programCube.use();
    glBindVertexArray(vao1);

    float f = currentTime * 4.0f;

    vmath::mat4 modelMatrix = vmath::scale(7.0f);
    vmath::vec3 viewPosition = vmath::vec3(cosf(f * 0.35f) * 120.0f, 45.0f, sinf(f * 0.35f) * 120.0f);
    vmath::mat4 viewMatrix = vmath::lookat(viewPosition,
                                            vmath::vec3(0.0f, 0.0f, 0.0f),
                                            vmath::vec3(0.0f, 1.0f, 0.0f));
    vmath::mat4 zoomMatrix = vmath::scale(6.00f, 6.00f, 6.00f);
    vmath::mat4 mvMatrix = viewMatrix * modelMatrix * zoomMatrix;
    vmath::mat4 projMatrix = vmath::perspective(48.0f,
                                                 getWindowRatio(),
                                                 0.1f,
                                                 1000.0f);

    programCube.setUniformMatrix4fv("mvp", (projMatrix * mvMatrix).operator float * ());

    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 36);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

void Depth::renderCircle(double currentTime, double difTime)
{
    glDisable(GL_DEPTH_TEST);

    programCircle.use();
    glBindVertexArray(vao2);

    const float near = -0.1f;
    const float far = 100.0f;
    const float r = getWindowRatio();
    const float k = 1.2f;
    auto perspective = vmath::ortho(-r * k, r * k, -k, k, near, far);

    programCircle.setUniform1f("near", near);
    programCircle.setUniform1f("far", far);
    programCircle.setUniformMatrix4fv("mvp", perspective.operator float * ());
    programCircle.setUniform1f("glassMaterial", 0.1);
    programCircle.setUniform2f("viewportSize", getWidth(), getHeight());
    programCircle.setUniform1i("cubeDepthTexture", 1);

    glDrawArrays(GL_TRIANGLE_FAN, 0, verticesMirror.size() / 3);
}

REGISTER_OBJECT(Node, Depth)
